Skip to content

fix(server): use weak comparison for If-None-Match - #2710

Merged
james-elicx merged 27 commits into
cloudflare:mainfrom
Boyeep:fix/weak-etag-validation
Jul 27, 2026
Merged

fix(server): use weak comparison for If-None-Match#2710
james-elicx merged 27 commits into
cloudflare:mainfrom
Boyeep:fix/weak-etag-validation

Conversation

@Boyeep

@Boyeep Boyeep commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

What changed

  • extract conditional ETag matching into a focused HTTP helper
  • use weak comparison for If-None-Match, as required by RFC 9110
  • parse comma-separated validators without splitting commas inside opaque tags
  • accept optional whitespace and valid empty list members while rejecting malformed tags and mixed wildcard lists
  • reuse the same parser for Pages Router page responses so conditional paths cannot drift

Why

The production static server previously compared entity tags as exact strings. A strong validator and weak validator with the same opaque tag therefore failed to match, causing a full 200 response instead of 304 Not Modified.

The Pages Router page-response path also had a separate comma-splitting matcher. Both paths now share the same validated parser.

Standards and Next.js parity

Weak comparison is symmetric and matches current Next.js behavior. RFC 9110 also permits commas and literal backslashes inside an opaque tag. This PR intentionally follows that grammar for comma-containing tags even though Next.js 16.2.7 currently delegates to a compiled fresh parser that splits every comma. Malformed fields fail conservatively instead of producing a false match.

Unsupported static request methods are handled separately by #2714; this PR remains scoped to entity-tag matching.

Impact

Static and buffered Pages Router conditional requests now share correct weak comparison without changing entity-tag case sensitivity. The parser is linear in the header length and only runs when If-None-Match is present.

Validation

  • vp test run tests/http-conditional.test.ts tests/serve-static.test.ts tests/pages-page-response.test.ts — 113 passed
  • vp check tests/http-conditional.test.ts tests/serve-static.test.ts tests/pages-page-response.test.ts packages/vinext/src/server/http-conditional.ts packages/vinext/src/server/prod-server.ts packages/vinext/src/server/pages-page-response.ts
  • vp run vinext#build
  • real HTTP proof against the built production static handler: strong/weak GET and HEAD matches returned 304; list and wildcard matches returned 304; malformed mixed wildcard returned 200; a matching ETag took precedence over Range; a mismatching ETag took precedence over a future If-Modified-Since value

@pkg-pr-new

pkg-pr-new Bot commented Jul 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2710
npm i https://pkg.pr.new/create-vinext-app@2710
npm i https://pkg.pr.new/@vinext/types@2710
npm i https://pkg.pr.new/vinext@2710

commit: a699cec

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 6b9e0e1 against base b779754 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 132.4 KB 132.4 KB ⚫ +0.0%
Client entry size (gzip) vinext 119.8 KB 119.8 KB ⚫ +0.0%
Dev server cold start vinext 2.82 s 2.79 s ⚫ -0.9%
Production build time vinext 2.93 s 2.92 s ⚫ -0.6%
RSC entry closure size (gzip) vinext 105.6 KB 105.6 KB ⚫ +0.0%
Server bundle size (gzip) vinext 179.8 KB 179.8 KB ⚫ +0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

james-elicx added a commit to Boyeep/vinext that referenced this pull request Jul 27, 2026
# Conflicts:
#	packages/vinext/src/server/prod-server.ts
james-elicx added a commit to Boyeep/vinext that referenced this pull request Jul 27, 2026
james-elicx added a commit to Boyeep/vinext that referenced this pull request Jul 27, 2026
@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(server): use weak comparison for If-None-Match

The core change — matchesIfNoneMatch in http-conditional.ts and its adoption in prod-server.ts — is excellent. It's a clean, RFC-9110-correct, linear-time parser, well tested in http-conditional.test.ts, and the weak-comparison fix is right. The serve-static.test.ts update correctly proves strong-vs-weak matching.

My main concern is scope. The PR title and body describe a focused entity-tag matching fix, but the diff also introduces a 551-line dev-server public-file ETag subsystem (dev-public-etag.ts) plus 42 lines of index.ts wiring and ~700 lines of tests for it. That's ~1300 of the 1475 added lines, and the PR description doesn't mention it at all. This is the bulk of the review surface and risk.

Scope / reviewability

  • The dev ETag indexing feature (Unicode case-folding, NFD/NFC normalization detection, symlink-cycle traversal, trie path resolution, filesystem case-sensitivity probing) is a substantial, independent feature. Per AGENTS.md ("For large refactors, prefer small stacked PRs" and keeping PRs reviewable), this should be split into its own PR with its own description explaining the motivation (dev/prod parity for If-None-Match on public/ files). As-is, a reviewer approving "a weak-comparison fix" is actually approving a large new dev-server code path.
  • If the two are genuinely coupled, the PR body should at minimum document the dev-server changes, the platform assumptions (much of the logic is darwin-gated in tests), and the startup-cost tradeoffs below.

Startup and watcher cost (dev only, but worth confirming)

See inline comments. createDevPublicFileEtags does a synchronous recursive realpathSync.native + statSync walk of the entire public/ tree at dev-server boot, plus detectCaseInsensitiveDirectory/detectNormalizationInsensitiveDirectory probes. And every watcher event that updateDevPublicFileEtag can't handle incrementally triggers a full rescan. For large public/ directories this could add noticeable cold-start latency and per-change churn. Worth a benchmark note or a comment acknowledging the tradeoff.

Correctness (minor)

One fail-closed branch in indexRequestPath looks slightly off (nulls realPath on a redirect conflict rather than the redirect) — see inline. It's safe (fails closed) but the intent should be documented.

Nothing blocking on the actual entity-tag fix — that part is ready. The ask is to separate/document the dev subsystem so it can be reviewed on its own merits.

Comment thread packages/vinext/src/index.ts Outdated

configureServer(server: ViteDevServer) {
const devPublicDir = server.config.publicDir || undefined;
let devPublicFileEtags = devPublicDir ? createDevPublicFileEtags(devPublicDir) : undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createDevPublicFileEtags(devPublicDir) runs synchronously here at dev-server startup and performs a full recursive fs.realpathSync.native + fs.statSync walk of the entire public/ tree, plus two directory case/normalization probes. For apps with large public/ directories this adds cold-start latency on the critical dev-boot path. Consider documenting the tradeoff, or building the index lazily / off the hot path. A benchmark note in the PR body would help reviewers weigh this.

Comment thread packages/vinext/src/index.ts Outdated
devPublicDir,
undefined,
undefined,
devPublicFileEtags.viteUsesStatLookup,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any watcher event updateDevPublicFileEtag can't handle incrementally causes a full createDevPublicFileEtags rescan of the whole public tree. With five watcher events wired (add/change/unlink/addDir/unlinkDir), directory-level changes and removals-behind-symlinks will each trigger a complete recursive rescan. For large public/ dirs this could be a lot of repeated sync fs work during active development. Worth confirming this is acceptable or debouncing/narrowing the rebuild trigger.

if (redirect) {
if (node.redirect === undefined || node.redirect === redirect) node.redirect = redirect;
else node.realPath = null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a conflicting redirect, this nulls node.realPath rather than the redirect itself, leaving node.redirect holding a stale value. It appears intentional (fail closed so the ambiguous node resolves nothing), but it's subtle — a reader would expect the conflicting redirect to be nulled. Please add a comment explaining why realPath is the field poisoned here.

expandedCaseTokens: string[];
};

const simpleCasePatterns = new Map<string, RegExp>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simpleCasePatterns is an unbounded module-level cache. In practice it's bounded by the set of distinct uppercase case-expansion tokens across public filenames, so it won't grow without limit, but a one-line comment noting the effective bound would preempt the "is this a leak?" question for the next reader.

@ask-bonk

ask-bonk Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Here's a summary of my findings on PR #2710:

Review Summary

The core fix is solid. matchesIfNoneMatch in http-conditional.ts is a clean, RFC-9110-correct, linear-time entity-tag parser with weak comparison, thoroughly tested. Its adoption in prod-server.ts and the serve-static.test.ts update are correct.

Main issue: scope creep. The PR title/body describe a focused ~88-line entity-tag matching fix, but the diff actually adds a 551-line dev-server public-file ETag subsystem (dev-public-etag.ts) plus index.ts wiring and ~700 lines of tests — roughly 1300 of the 1475 added lines. This large, independent feature (Unicode case-folding, NFD/NFC normalization detection, symlink-cycle traversal, trie path resolution, filesystem case-sensitivity probing) is never mentioned in the PR description. Per AGENTS.md's guidance on small, reviewable stacked PRs, I recommended splitting or at least documenting it.

Secondary concerns (all dev-only, non-blocking):

  1. createDevPublicFileEtags does a synchronous recursive realpathSync/statSync walk of the whole public/ tree at dev-server startup — cold-start cost.
  2. Every unhandled watcher event triggers a full index rescan — per-change churn for large public/ dirs.
  3. A fail-closed branch in indexRequestPath nulls realPath on a redirect conflict (safe but under-documented).
  4. simpleCasePatterns is an unbounded module cache (effectively bounded, but worth a comment).

I posted this as a single COMMENT review (not requesting changes) with 4 inline comments, since the actual entity-tag fix is ready and the asks are about separation/documentation of the dev subsystem.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(server): use weak comparison for If-None-Match

Reviewed the diff against the true merge base — this PR is exactly the 4 files it claims (+147/-14): http-conditional.ts, prod-server.ts, and the two test files. The static-file-cache.ts deltas that show up in a raw local origin/main diff came from #2713, which is already on main.

Verified locally

  • vp test run tests/http-conditional.test.ts → 13 passed
  • vp test run tests/serve-static.test.ts → 47 passed (60 total, matches the PR body)
  • vp check on all 4 files → format, lint, type all clean

The fix is correct

  • matchesIfNoneMatch implements RFC 9110 weak comparison (strips W/ on both the current tag and each candidate before comparing opaque tags), which is the actual bug fix — a strong If-None-Match now matches the weak ETags this server generates.
  • The etagc grammar (%x21 / %x23-7E / obs-text) is right: it excludes 0x20 (space), 0x22 ("), and 0x7F (DEL), which the malformed-tag tests confirm.
  • All three prod-server.ts call sites always pass a well-formed quoted ETag (either etagFromFilenameHashW/"<hash>" or the mtime fallback W/"<size>-<mtime>"), so the current-tag validation on lines 10–11 never spuriously rejects a real server ETag.
  • Comma-in-opaque-tag handling and wildcard-in-list rejection are both correct and tested.

Note on the earlier /bigbonk review

The prior automated review flagged a 551-line dev-public-etag.ts subsystem plus index.ts wiring. That code is not in this PR — the final commit d0d7dbb ("refactor(dev): leave public etag handling to Vite") removed it. The earlier review was reading an intermediate state of the branch. All of its concerns (startup walk cost, watcher rescans, the realPath fail-closed branch, the simpleCasePatterns cache) are moot against the current head.

Minor (non-blocking)

One behavioral nuance not covered by a test: within a comma list, matched ||= … keeps parsing after a match, so a valid match followed by a malformed token (e.g. '"asset", garbage') returns false → full 200, rather than honoring the earlier match. This fails conservatively (never a false 304) and the PR body already documents "malformed fields fail conservatively," but it is a subtle divergence from Next.js's fresh (which splits on comma and would still 304). A one-line test pinning this intent would make it explicit for future readers.

Nothing blocking. The entity-tag fix is ready.

Comment thread packages/vinext/src/server/http-conditional.ts
@ask-bonk

ask-bonk Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review posted. Summary of my findings on PR #2710:

Verdict: the fix is correct and ready

The core change is solid. matchesIfNoneMatch in http-conditional.ts correctly implements RFC 9110 weak comparison — it strips the W/ prefix from both the current ETag and each candidate before comparing opaque tags, which fixes the reported bug (strong If-None-Match failing to match the weak ETags this server generates). The etagc grammar, comma-in-opaque-tag handling, and wildcard-in-list rejection are all correct.

Verified locally:

  • 13 + 47 = 60 tests pass (matches PR body)
  • vp check clean on all 4 files (format, lint, type)
  • Confirmed all 3 prod-server.ts call sites always pass well-formed quoted ETags, so current-tag validation never spuriously rejects

Key finding — the earlier /bigbonk review is obsolete. That review flagged a 551-line dev-public-etag.ts subsystem and index.ts wiring as "scope creep." That code is not in this PR: the final commit d0d7dbb removed it entirely ("refactor(dev): leave public etag handling to Vite"). The earlier review read an intermediate branch state. Against the real merge base, the diff is exactly 4 files (+147/-14), a genuinely focused fix. All of that review's concerns are moot.

One minor non-blocking note: within a comma list, a valid match followed by a malformed token returns false (200) rather than honoring the earlier match — fails conservatively but diverges subtly from Next.js's fresh. Suggested a test to pin the intended behavior.

Posted as a COMMENT review (not requesting changes) with one inline suggestion.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(server): use weak comparison for If-None-Match

Reviewed against the true merge base — the diff is exactly the 4 files claimed (+147/-14). The dev-public-etag.ts subsystem flagged by an earlier /bigbonk run was removed in d0d7dbb and is not part of this PR; those concerns are moot.

The fix is correct

Verified locally:

  • vp test run tests/http-conditional.test.ts → 13 passed
  • vp check on all 4 files → format, lint, type clean

matchesIfNoneMatch implements RFC 9110 weak comparison correctly (strips W/ from both the current tag and each candidate before comparing opaque tags), which is the actual bug fix. The etagc grammar (%x21 / %x23-7E / obs-text), comma-in-opaque-tag handling, wildcard-in-list rejection, empty-member tolerance, and strict current-tag validation are all correct. I confirmed all three prod-server.ts call sites always pass a well-formed quoted ETag (etagFromFilenameHashW/"<hash>" or the W/"<size>-<mtime>" fallback), so the stricter current-tag validation on line 11 never spuriously rejects a real server ETag. The serve-static.test.ts change correctly proves strong-vs-weak matching. The '"match", garbage' case on line 49 already pins the conservative-fail behavior for a valid match followed by a malformed member, so the divergence-from-fresh nuance is documented and tested.

Non-blocking: a second, divergent matcher still exists

See inline comment. This PR replaces the naive split(",") matcher on the static path, but the Pages Router page path keeps its own etagMatches that still uses exactly the split(",") + W/-strip approach this PR argues against. Worth a follow-up so the two conditional paths don't drift (AGENTS.md dev/prod parity guidance). Pre-existing, not introduced here.

Nothing blocking — the entity-tag fix is ready.

Comment thread packages/vinext/src/server/prod-server.ts
@ask-bonk

ask-bonk Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Summary of my findings on PR #2710:

Verdict: the fix is correct and ready

Core change is solid. matchesIfNoneMatch in http-conditional.ts correctly implements RFC 9110 weak comparison — it strips the W/ prefix from both the current ETag and each candidate before comparing opaque tags, fixing the reported bug where a strong If-None-Match failed to match the weak ETags this server generates (returning 200 instead of 304).

Verified locally:

  • vp test run tests/http-conditional.test.ts → 13 passed
  • vp check on all 4 files → format, lint, type clean
  • Confirmed all 3 prod-server.ts call sites always pass well-formed quoted ETags (etagFromFilenameHashW/"<hash>" or the W/"<size>-<mtime>" fallback), so the stricter current-tag validation never spuriously rejects a real server ETag.

The diff is genuinely focused — exactly the 4 files claimed (+147/-14). The dev-public-etag.ts subsystem flagged by an earlier /bigbonk run was removed in commit d0d7dbb; those concerns are moot against the current head. The "match followed by malformed member" conservative-fail behavior (diverges subtly from fresh) is already pinned by the '"match", garbage' test case.

One non-blocking finding I raised inline: There's a second, parallel If-None-Match matcher — etagMatches in pages-page-response.ts:70 — used by the Pages Router page path. It still uses the naive split(",") + W/-strip approach that this PR argues against on the static path. Not a regression (pre-existing), but now that a correct shared helper exists, a follow-up to route both conditional paths through it would prevent drift, per AGENTS.md's dev/prod parity guidance.

Posted as a COMMENT review (not requesting changes) with one inline follow-up suggestion.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

UnknownError: ProviderInitError

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(server): use weak comparison for If-None-Match

Verified independently against the true merge base — the diff is exactly the 4 source/test files claimed (+152/-30). The earlier /bigbonk run's dev-public-etag.ts scope-creep concerns are moot (that code was removed in d0d7dbb).

The fix is correct

  • matchesIfNoneMatch implements RFC 9110 weak comparison correctly: it strips W/ from both the current ETag and each candidate before comparing opaque tags, so a strong If-None-Match now matches the weak ETags this server generates. That is the actual bug fix.
  • The etagc grammar (%x21 / %x23-7E / obs-text) is right — it excludes 0x20, 0x22, and 0x7F, confirmed by the malformed-tag tests.
  • Comma-in-opaque-tag handling, wildcard-in-list rejection, empty-member tolerance, and strict current-tag validation are all correct.
  • I traced every producer of the etag argument: etagFromFilenameHash (W/"<hash>"), the static W/"<size>-<mtime>" fallback, and generatePagesETag (strong "..."). All are well-formed, so the strict current-tag validation on line 11 never spuriously rejects a real server ETag.

Verified locally

  • tests/http-conditional.test.ts → 13 passed
  • tests/serve-static.test.ts → 48 passed
  • tests/pages-page-response.test.ts → 53 passed

I also probed additional edge cases (adjacent tags without a comma, double W/, trailing comma, whitespace-only field, ,-only field) — all fail closed or behave per RFC as expected.

Non-blocking observations

  1. The etagMatches(etag, ifNoneMatch) wrapper flips argument order relative to matchesIfNoneMatch(ifNoneMatch, etag). The delegation is correct, but the flip is a mild footgun — see inline note.
  2. Behavior for a wildcard is now stricter than the old ifNoneMatch === "*" check: the new parser requires the * (plus OWS) to consume the whole field, so "*x" no longer matches. That is more RFC-correct, not a regression, but it is a silent behavioral change on the Pages path worth being aware of.

Nothing blocking — the entity-tag fix is ready.

Comment thread packages/vinext/src/server/pages-page-response.ts Outdated
@ask-bonk

ask-bonk Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Summary of my findings on PR #2710:

Verdict: the fix is correct and ready

Core change is solid. matchesIfNoneMatch in http-conditional.ts correctly implements RFC 9110 weak comparison — stripping W/ from both the current ETag and each candidate before comparing opaque tags. This fixes the reported bug where a strong If-None-Match failed to match the weak ETags the server generates (returning 200 instead of 304).

Independently verified:

  • The etagc grammar (%x21 / %x23-7E / obs-text) is correct — excludes space, ", and DEL.
  • Traced every producer of the etag argument (etagFromFilenameHashW/"<hash>", the W/"<size>-<mtime>" fallback, and generatePagesETag → strong "..."). All are well-formed, so the strict current-tag validation never spuriously rejects a real ETag.
  • Ran all 3 test suites locally: 13 + 48 + 53 = 114 passed.
  • Probed extra edge cases (adjacent tags without comma, double W/, trailing comma, whitespace-only field) via a standalone reimplementation — all fail closed / RFC-correct.

The diff is genuinely focused — exactly the 4 files claimed (+152/-30). The dev-public-etag.ts subsystem an earlier /bigbonk run flagged as scope creep was removed in d0d7dbb and is not part of this PR.

Two non-blocking observations (posted, not requesting changes):

  1. etagMatches(etag, ifNoneMatch) flips argument order vs matchesIfNoneMatch(ifNoneMatch, etag) — correct delegation but a mild footgun worth a comment (inline suggestion posted).
  2. Wildcard handling is now stricter (* must consume the whole field), so "*x" no longer matches — more RFC-correct, not a regression, but a silent behavioral change on the Pages path.

Posted as a COMMENT review with one inline note.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(server): use weak comparison for If-None-Match

Verified against the current head a699cec. The diff is the 4 source files + 3 test files claimed. The dev-public-etag.ts subsystem an earlier /bigbonk run flagged as scope creep was removed in d0d7dbb and is genuinely not part of this PR.

The fix is correct

matchesIfNoneMatch in http-conditional.ts implements RFC 9110 weak comparison correctly — it strips W/ from both the current ETag and each candidate before comparing opaque tags, which fixes the reported bug (a strong If-None-Match now matches the weak ETags this server generates, so 304 instead of a full 200).

  • The etagc grammar on line 59 (%x21 / %x23-7E / obs-text) is right: it excludes 0x20, 0x22 ("), and 0x7F (DEL), confirmed by the malformed-tag tests.
  • Comma-in-opaque-tag handling, wildcard-in-list rejection, empty-member tolerance, and the strict current-tag validation are all correct.
  • Traced every producer of the etag argument: etagFromFilenameHash (W/"<hash>"), the static W/"<size>-<mtime>" fallback (prod-server.ts:699, static-file-cache.ts:160), and generatePagesETag (strong "..."). All are well-formed, so the stricter current-tag validation on line 11 never spuriously rejects a real server ETag.
  • Both Pages Router paths (pages-page-data.ts:976, pages-page-response.ts) and all three static call sites now delegate to the shared parser — the parity concern from prior reviews is resolved.

Verified locally

  • vp test run tests/http-conditional.test.ts tests/pages-page-response.test.ts tests/serve-static.test.ts → 113 passed (matches the PR body)
  • vp check on all 4 source files → format, lint, type all clean

Non-blocking observation

One subtle semantic narrowing on the wildcard path — see inline. Not a live bug because all callers pass well-formed ETags, but worth a note for future callers.

Nothing blocking. The entity-tag fix is ready.

const currentOpaqueTag = etag.slice(current.opaqueStart, current.opaqueEnd);

let index = skipOptionalWhitespace(ifNoneMatch, 0);
if (ifNoneMatch[index] === "*") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subtle semantic narrowing vs. the old matchesIfNoneMatchHeader: the wildcard branch is now gated behind the current-ETag validation on lines 10-11, so If-None-Match: * returns false when etag fails parseEntityTag. RFC 9110 defines * as matching whenever a current representation exists, independent of the ETag's exact form, and the old code returned true for * unconditionally.

This is not a live bug — every caller passes a well-formed W/"..." or "..." ETag (verified all producers), so the wildcard path is never reached with an invalid etag. But if a future caller ever passes a non-quoted or malformed ETag, * would silently stop matching. A one-line comment noting that * intentionally requires a valid current ETag here (or hoisting the * check above the current-tag parse) would preempt the question.

@ask-bonk

ask-bonk Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully. Summary of my findings on PR #2710:

Verdict: the fix is correct and ready

Core change is solid. matchesIfNoneMatch in http-conditional.ts correctly implements RFC 9110 weak comparison — stripping the W/ prefix from both the current ETag and each candidate before comparing opaque tags. This fixes the reported bug where a strong If-None-Match failed to match the weak ETags the server generates (returning 200 instead of 304).

Independently verified against current head a699cec:

  • The diff is exactly the 4 source + 3 test files claimed. The dev-public-etag.ts scope-creep subsystem from earlier reviews was removed in d0d7dbb and is not in this PR.
  • Ran the 3 focused suites → 113 passed (matches PR body).
  • vp check on all 4 source files → format, lint, type clean.
  • Traced every producer of the etag argument (etagFromFilenameHashW/"<hash>", the W/"<size>-<mtime>" fallback, generatePagesETag → strong "..."). All are well-formed, so the stricter current-tag validation never spuriously rejects a real ETag.
  • Confirmed both Pages Router paths and all three static call sites now delegate to the shared parser — the prior parity concern is resolved.

One non-blocking inline note: The * wildcard branch is now gated behind the current-ETag validation (lines 10-11), so If-None-Match: * returns false if etag is malformed — a subtle narrowing vs. the old code's unconditional * match. Not a live bug (all callers pass well-formed ETags), but worth a comment for future callers.

Posted as a COMMENT review (not requesting changes) with one inline observation.

github run

@james-elicx
james-elicx merged commit c9a4a84 into cloudflare:main Jul 27, 2026
58 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants